﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace LunarSF.SHomeWorkshop.LunarMarkdownEditor.Utils
{
    /// <summary>
    /// From: <https://revealjs.com>
    /// reveal.js is an open source HTML presentation framework. 
    /// It's a tool that enables anyone with a web browser to create 
    /// fully-featured and beautiful presentations for free.
    /// </summary>
    public partial class RevealJsTools
    {
        public static void Show()
        {
            var awtvi = Globals.MainWindow.ActiveWTVI;
            if (awtvi == null)
            {
                var ae = Globals.MainWindow.ActivedEditor;
                if (ae == null)
                {
                    LMessageBox.ShowInfomation("请在工作区管理器中选择要演示的文档。");
                    return;
                }
                else
                {
                    awtvi = Globals.MainWindow.FindWorkspaceTreeViewItem(ae.FullFilePath);
                    if (awtvi == null)
                    {
                        LMessageBox.ShowInfomation("请在工作区管理器中选择要演示的文档。");
                        return;
                    }
                    else
                    {
                        awtvi.IsChecked = true;
                    }
                }
            }

            if (awtvi.ItemType != WorkspaceTreeViewItem.Type.File && awtvi.ItemType != WorkspaceTreeViewItem.Type.Folder)
            {
                LMessageBox.ShowInfomation("请在工作区管理器中选择普通 Markdown 文件或目录（元文件）进行演示。");
                return;
            }

            if (awtvi.IsModified)
            {
                LMessageBox.ShowWarning("工作区中选定的文档已打开且已被修改。调用此功能需要先保存文件。");
                return;
            }

            // 基本思路：
            // 1. 先检查工作区下有没有“_reveal-js”目录。
            //    如果没有，就从安装目录下复制到工作区目录。
            // 2. 根据模板进行填充，生成当前目录下的对应演示文件（演示文件名与一般生成的 Html 文件名相比，多个后缀“~reveal.js~”）。
            //    也就是说，如果 Markdown 文件名为 “x.md”，则通常编译出的 Html 文件名为“x.html”——而调用 reveal.js 演示的文件名则为“x~reveal.js~.html”
            // 3. 根据用户在当前 LME 文档中设置的注释，设置theme。
            // 4. 通过操作系统调用默认浏览器演示。
            //    不通过内置浏览器是为了更好地发挥 reveal.js 的特效——WPF 内置的（实际上是 WinForm 封闭的） IE 内核版本太低。

            // 1. 先检查工作区下有没有“_reveal-js”目录
            if (System.IO.Directory.Exists(Globals.PathOfWorkspaceRevealJs) == false)
            {
                var answer = LMessageBox.ShowQuestion($"使用此功能需要向当前工作区复制 reveal.js 第三方库，大约需要占据 6MB 磁盘空间。\r\n\r\n　　要继续吗？");
                if (answer != System.Windows.MessageBoxResult.Yes) return;

                if (false == Utils.LDirectory.CopyDirectory(Globals.InstalledPath + "Workspace\\_reveal-js\\", Globals.PathOfWorkspaceRevealJs, true))
                {
                    LMessageBox.ShowWarning("向当前工作区引入 reveal.js 第三方库失败！");
                    return;
                }
            }

            // 2. 根据模板进行填充，生成当前目录下的对应演示文件（演示文件名与一般生成的 Html 文件名相比，多个后缀“~reveal.js~”）。
            //    也就是说，如果 Markdown 文件名为 “x.md”，则通常编译出的 Html 文件名为“x.html”——而调用 reveal.js 演示的文件名则为“x~reveal.js~.html”

            var awtviRelativePath = awtvi.RelativePath;
            var layers = awtviRelativePath.Split(new char[] { '\\', }, StringSplitOptions.RemoveEmptyEntries);
            var relative_path = "";
            for (int i = 1; i < layers.Length; i++)
            {
                relative_path += "../";
            }

            if (relative_path.EndsWith("_reveal-js/") == false)
            {
                relative_path += "_reveal-js/";
            }

            var templateFilePath = Globals.InstalledPath + "Workspace\\_reveal-js\\_template.html";
            if (System.IO.File.Exists(templateFilePath) == false)
            {
                LMessageBox.ShowWarning("没有找到 reveal.js 模板文件。请尝试重新安装本程序最新版本。");
                return;
            }

            // * 每个页面都是一个方块，方块标头样式表中写字体、前景色（深色字|浅色字）、背景色、加粗等
            // * 其它特效设置不放在样式表中，放在标尾文本（或者在头部以 ；注释的形式书写（因为对文本文档无意义）
            // * 方块内部可以再嵌套一层（垂直子页）
            //      如果方块内有子层，则子层间会被忽略——这与顶层幻灯片间的其它内容会被忽略是一样的。
            // * 幻灯片应该支持的 Markdown 元素包括：
            //      1. 方块区域（自动解释为 &lt;section&gt;...&lt;/section&gt;）
            //      2. 标题（本质是带 class 的段落 其中 Setext 样式的标题只支持两层）
            //      3. 普通段落与基本格式符（加粗、倾斜、下划线、删除线、行内代码块【fragments】）
            //      4. 列表（无序、有序）
            //      5. 图像链接
            //      6. 代码块（含树型文字表）
            //      7. 引用块
            //      8. 二维文字表
            //      9. 试题（选择或判断、填空、材料解析）

            var mdFilePath = "";
            switch (awtvi.ItemType)
            {
                case WorkspaceTreeViewItem.Type.File:
                    {
                        mdFilePath = awtvi.FullPath;
                        break;
                    }
                case WorkspaceTreeViewItem.Type.Folder:
                    {
                        mdFilePath = awtvi.MetaFilePath;
                        break;
                    }
                default:
                    {
                        mdFilePath = null;
                        break;
                    }
            }

            if (System.IO.File.Exists(mdFilePath) == false)
            {
                LMessageBox.ShowWarning("在存储器中没有找到要演示的 Markdown 文件！");
                return;
            }

            var mdFileContent = System.IO.File.ReadAllText(mdFilePath, Encoding.UTF8);

            // 调用 Reveal.js 前先先处理试题——否则编号顺序会出错。
            mdFileContent = Question.ConvertQuestionsToHtml(mdFileContent, true);

            // 两层嵌套的方块较为复杂，不如使用水平线进行分隔
            // 但使用水平线分隔，又会与 Setext 样式的标题冲突
            // 所以，还是用两层方块来表示吧，虽然麻烦些，但是可以留下一些空间另做它用。

            var theme = "night";
            var transition = "none";
            var revealOptions = new Dictionary<string, string>();

            var slides = ReadContentOfSlides(mdFileContent, ref revealOptions);
            if (slides == null || slides.Count <= 0)
            {
                LMessageBox.ShowWarning("在当前 Markdown 文件中没有找到可以演示的内容！");
                return;
            }

            var templateContent = System.IO.File.ReadAllText(templateFilePath);

            if (string.IsNullOrWhiteSpace(templateContent) || templateContent.Contains("{relative_path}") == false)
            {
                LMessageBox.ShowWarning("reveal.js 模板文件没有有效内容。请尝试重新安装本程序最新版本。");
                return;
            }

            // 3. 根据用户在当前 LME 文档中设置的注释，设置theme等信息。
            //    这个在读取文件内容后实现。
            var title = awtvi.Title;

            if (revealOptions != null)
            {
                // 幻灯片主题
                if (revealOptions.ContainsKey("theme") && string.IsNullOrWhiteSpace(revealOptions["theme"]) == false)
                {
                    theme = revealOptions["theme"];
                }
                else
                {
                    if (Globals.MainWindow.cmbColor.SelectedIndex == 0)
                    {
                        theme = "white";
                    }
                    else
                    {
                        theme = "night";
                    }
                }

                // 幻灯片切换模式
                if (revealOptions.ContainsKey("transition") && string.IsNullOrWhiteSpace(revealOptions["transition"]) == false)
                {
                    transition = Transition.GetTransitionName(revealOptions["transition"]);
                }
            }

            // 这些替换不能放到 if{...}中
            templateContent = templateContent.Replace("{relative_path}", relative_path).Replace("{theme}", theme)
                                             .Replace("{title}", title);
            if (string.IsNullOrWhiteSpace(transition) == false)
            {
                templateContent = templateContent.Replace("{backgroundTransition}", $"backgroundTransition : '{transition}',\r\n");
            }

            #region 编译代表各幻灯片的 sections

            var slidesContentText = new StringBuilder();

            foreach (var slide in slides)
            {
                slidesContentText.Append(slide.Html);
            }

            #endregion 编译 sections

            var htmlDocumentText = templateContent.Replace("{sections}", slidesContentText.ToString());
            var doc = NSoup.NSoupClient.Parse(htmlDocumentText);
            htmlDocumentText = doc.OuterHtml();

            using (var sw = new System.IO.StreamWriter(awtvi.RevealJsHtmlFullPath))
            {
                sw.Write(htmlDocumentText);
            }

            // 4. 通过操作系统调用默认浏览器演示。
            //    不通过内置浏览器是为了更好地发挥 reveal.js 的特效——WPF 内置的（实际上是 WinForm 封闭的） IE 内核版本太低。
            System.Diagnostics.Process.Start(awtvi.RevealJsHtmlFullPath);
        }

        /// <summary>
        /// 解析（读取）各方块区域。
        /// </summary>
        /// <param name="mdFileContent"></param>
        /// <returns></returns>
        private static List<RevealJsSlide> ReadContentOfSlides(string mdFileContent, ref Dictionary<string, string> options)
        {
            if (string.IsNullOrWhiteSpace(mdFileContent)) return null;

            var lines = mdFileContent.Split(new string[] { "\r\n", }, StringSplitOptions.None);

            var slides = new List<RevealJsSlide>();

            RevealJsSlide slide = null;
            RevealJsInnerSlide iSlide = null;
            var trimChars = new char[] { ' ', '　', '\t', };

            bool slideBegin = false;

            foreach (var line in lines)
            {
                if (slideBegin == false)  // 全局定义必须写在第一个幻灯片（方块区域）之前。
                {
                    if (ReadRevealJSConfigLine(line, ref options)) continue;
                }

                if (line.StartsWith("[") && line.Contains("]") == false)
                {
                    slideBegin = true;  // 一旦开始一个幻灯片，全局选项就不应该再起作用。

                    // 方块区域头
                    if (slide == null)
                    {
                        slide = new RevealJsSlide();
                        slides.Add(slide);

                        var index = line.IndexOf("$");
                        if (index >= 1)
                        {
                            slide.HeaderStyleText = line.Substring(index + 1);
                        }
                    }
                    else
                    {
                        iSlide = new RevealJsInnerSlide();
                        slide.InnerSlides.Add(iSlide);

                        var index = line.IndexOf("$");
                        if (index >= 1)
                        {
                            iSlide.HeaderStyleText = line.Substring(index + 1);
                        }
                    }
                }
                else if (line.StartsWith("]"))
                {
                    // 方块区域尾
                    if (iSlide != null)
                    {
                        var index = line.IndexOf("$");
                        if (index >= 1)
                        {
                            iSlide.TailStyleText = line.Substring(index + 1);
                        }
                        iSlide = null;
                    }
                    else if (slide != null)
                    {
                        var index = line.IndexOf("$");
                        if (index >= 1)
                        {
                            slide.TailStyleText = line.Substring(index + 1);
                        }
                        slide = null;
                    }
                }
                else
                {
                    if (iSlide != null)
                    {
                        if (line.StartsWith(";") || line.StartsWith("；"))
                        {
                            if (iSlide.ReadSlideOptionLine(line) == false)
                            {
                                iSlide.NoteLinesText.Append(line.Substring(1).Trim(trimChars));
                                iSlide.NoteLinesText.Append("\r\n");
                            }
                            else continue;
                        }
                        if (line.StartsWith("!") || line.StartsWith("！"))
                        {
                            iSlide.ContentMarkdownText.Append("    " + line);  // 树型文字表，视为代码块
                            iSlide.ContentMarkdownText.Append("\r\n");
                        }
                        else
                        {
                            iSlide.ContentMarkdownText.Append(line);
                            iSlide.ContentMarkdownText.Append("\r\n");
                        }
                    }
                    else if (slide != null)
                    {
                        if (line.StartsWith(";") || line.StartsWith("；"))
                        {
                            if (slide.ReadSlideOptionLine(line) == false)
                            {
                                slide.NoteLinesText.Append(line.Substring(1).Trim(trimChars));
                                slide.NoteLinesText.Append("\r\n");
                            }
                            else continue;
                        }
                        if (line.StartsWith("!") || line.StartsWith("！"))
                        {
                            slide.ContentMarkdownText.Append("    " + line);  // 树型文字表，视为代码块
                            slide.ContentMarkdownText.Append("\r\n");
                        }
                        else
                        {
                            slide.ContentMarkdownText.Append(line);
                            slide.ContentMarkdownText.Append("\r\n");
                        }
                    }
                }
            }

            return slides;
        }

        /// <summary>
        /// 读取以分号开头的幻灯片选项配置文本。
        /// </summary>
        internal static bool ReadRevealJSConfigLine(string line, ref Dictionary<string, string> options)
        {
            var trimChars = new char[] { ' ', '　', '\t', };

            if (line.StartsWith(";") || line.StartsWith("；"))
            {
                var lowerLine = line.ToLower();

                // 幻灯片主题选项
                var regRevealTheme = new Regex(@"^[;；][ 　\t]*?(theme|主题)[ 　\t]*?[:：][ 　\t]*?");
                var matchRevealTheme = regRevealTheme.Match(lowerLine);
                if (matchRevealTheme.Success)
                {
                    WriteOption(ref options, "theme", lowerLine.Substring(matchRevealTheme.Length).Trim(trimChars));
                    return true;
                }

                // 幻灯片切换模式
                var regRevealTransition = new Regex(@"^[;；][ 　\t]*?((transition)|(切换(模式)?))[ 　\t]*?[:：][ 　\t]*?");
                var matchRevealTransition = regRevealTransition.Match(lowerLine);
                if (matchRevealTransition.Success)
                {
                    WriteOption(ref options, "transition", lowerLine.Substring(matchRevealTransition.Length).Trim(trimChars));
                    return true;
                }


                // ... 基于开发成本和“开箱即用，省去配置麻烦”的考虑，不需要支持所有选项。
            }

            return false;
        }

        internal static void WriteOption(ref Dictionary<string, string> options, string key, string cvalue)
        {
            if (string.IsNullOrWhiteSpace(key)) return;

            var trimChars = new char[] { ' ', '　', '\t', };

            if (options.ContainsKey(key))
            {
                options[key] = cvalue.Trim(trimChars);
            }
            else
            {
                options.Add(key, cvalue.Trim(trimChars));
            }
            return;
        }

        /// <summary>
        /// 导出可用于 reveal.js 演示的 网页文件（包括图像资源）。
        /// </summary>
        /// <returns></returns>
        public static bool OutportRevealJsHtml()
        {
            LMessageBox.ShowInfomation("惭愧惭愧，这个功能还没实现呢！最近太忙了。");
            return true;

            // 基本思路：
            // 1. 向此 reveal.js 目录下导出当前文档中 reveal-slide 页面中引用的所有图像文件。
            // 2. 向用户选定的 reveal.js 目录下导出文件（通常选择在工作区下那个 _reveal-js 目录就行）。
            //    在导出此文件时，要注意所有 reveal-slide 页面中引用的图像链接的相对路径必须做相应修改。
            //    要能支持不同图像资源目录的合并和路径自动转换！！！
        }
    }
}
